Skip to content

crypto: drop unreachable has_exception() guards in Bun.password verify and pbkdf2 - #34794

Closed
robobun wants to merge 1 commit into
mainfrom
claude/farm/12a39d09/crypto-dead-exception-guards
Closed

robobun wants to merge 1 commit into
mainfrom
claude/farm/12a39d09/crypto-dead-exception-guards

Conversation

@robobun

@robobun robobun commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator

What

Removes three !global.has_exception() guards whose else-branches are provably unreachable:

  • js_password_object_verify / js_password_object_verify_sync (PasswordObject.rs): the preceding arguments[2].get_zig_string(global_object)? propagates any exception via ?, and algorithm_from_zig_string is a sequence of ZigString::eql_comptime byte compares with no JS entry. has_exception() is always false at the guard, so the Err(JsError::Thrown) / Ok(JSValue::ZERO) fallthroughs are dead. The JsError import had no other use in the file.
  • PBKDF2::from_js (PBKDF2.rs): the preceding arg4.to_slice(global_this)? propagates any exception via ?; evp::lookup_ignore_case is a ComptimeStringMap probe and Algorithm::md() returns a static BoringSSL singleton pointer. has_exception() is always false and return Err(JsError::Thrown) is dead. The digest slice is hoisted one scope out so the error message reuses it instead of calling to_slice() a second time.

Why

These guards are leftovers from the pattern where a fallible helper returned an in-band sentinel (None/.zero) with the exception left pending for the caller to check. In the Rust bindings, ? on JsResult already propagates the exception, so the explicit has_exception() check always observes false and the alternate branch is unreachable. Keeping them suggests a pending-exception state that cannot exist and invites readers to audit for one.

Verification

No behavior change. Existing coverage exercises every touched path:

  • test/js/bun/util/password.test.ts "invalid algorithm throws" passes "scrpyt" to both verify and verifySync, hitting the None arm of algorithm_from_zig_string.
  • test/js/node/crypto/pbkdf2.test.ts "invalid inputs > digest" passes "md55", hitting the 'invalid fallthrough and asserting Invalid digest: md55.

Both suites pass on this branch (106 pass, 0 fail). Ran the password suite under BUN_JSC_validateExceptionChecks=1 as well.

Because the removed branches are dead, there is no input that distinguishes before from after, so this change cannot produce a fail-before/pass-after test. Related items from the same audit (the redundant & 0x3F mask on bcrypt cost, and the explicit drop() calls in async verify) are left out here since #33865 and #33702 already rewrite those lines as part of behavioral fixes.

…y and pbkdf2

In both Bun.password.verify/verifySync and PBKDF2::from_js, the preceding
get_zig_string()/to_slice() call already propagates any JS exception via
the ? operator. The code that follows (algorithm_from_zig_string /
evp::lookup_ignore_case / Algorithm::md) is pure byte comparison and
static-table lookup with no JS entry, so has_exception() is always false
at the guard and the fallthrough branches (Err(JsError::Thrown) /
Ok(JSValue::ZERO)) are dead.

Hoist the pbkdf2 digest slice out of the 'invalid block so the error
message reuses it instead of calling to_slice() a second time.

No behavior change.
@robobun

robobun commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator Author

Status: dead-code cleanup in src/runtime/crypto/{PasswordObject,PBKDF2}.rs, removing has_exception() guards whose alternate branches are provably unreachable (? on the preceding JsResult already propagates, and the lookup helpers in between have no JS entry). No behavior change; existing password.test.ts / pbkdf2.test.ts exercise the touched paths and pass. A fail-before test is structurally not possible here, so this needs a maintainer to merge on review.

@robobun

robobun commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator Author
Updated 1:22 AM PT - Jul 20th, 2026

@robobun, your commit 5197150 is building: #76150

@coderabbitai

coderabbitai Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@robobun, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 29 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: ef8b7c8b-ce2f-4bc1-be3d-bb9022d9919e

📥 Commits

Reviewing files that changed from the base of the PR and between f6c9474 and 5197150.

📒 Files selected for processing (2)
  • src/runtime/crypto/PBKDF2.rs
  • src/runtime/crypto/PasswordObject.rs

Comment @coderabbitai help to get the list of available commands.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — dead-code removal with no behavior change.

What was reviewed:

  • Traced algorithm_from_zig_string → ZigString::eql_comptime (pure byte compare, no JS entry) and confirmed the preceding get_zig_string(...)? propagates any exception, so has_exception() is always false at both verify sites.
  • Traced evp::lookup_ignore_case (ComptimeStringMap probe) and Algorithm::md() (static BoringSSL singleton match) — neither can throw, so the PBKDF2 guard's else-branch is unreachable.
  • Checked the hoisted slice in PBKDF2::from_js stays live for the error-message use and drops correctly on break 'brk; the removed JsError import has no other use in the file.
Extended reasoning...

Overview

This PR removes three !global.has_exception() guards in src/runtime/crypto/PasswordObject.rs (js_password_object_verify, js_password_object_verify_sync) and src/runtime/crypto/PBKDF2.rs (PBKDF2::from_js). In each case the preceding fallible call (get_zig_string / to_slice) already propagates exceptions via ? on JsResult, and the code between that call and the guard is pure Rust (byte comparisons, a static string map lookup, a match returning BoringSSL const singletons) with no JS entry point. The else-branches (Err(JsError::Thrown) / Ok(JSValue::ZERO)) were therefore unreachable. The PBKDF2 change also hoists the slice binding one scope out so the error message reuses it instead of calling to_slice() a second time, and drops the now-unused JsError import.

Security risks

None. This is dead-code removal in argument-validation error paths. No new inputs are accepted, no validation is relaxed, and the crypto operations themselves (argon2/bcrypt/PBKDF2) are untouched.

Level of scrutiny

Low-to-moderate. The change is small (net −18 lines) and mechanical, but it touches crypto binding code, so I verified each unreachability claim against source: ZigString::eql_comptime at src/bun_core/string/mod.rs:1674 is a pure encoding-aware byte compare; evp::lookup_ignore_case at src/runtime/crypto/EVP.rs:160 is a ComptimeStringMap probe; Algorithm::md() at src/sha_hmac/sha.rs:248 is a match over static EVP_* getters. None of these can set a JS exception, so has_exception() is provably false after the preceding ?.

Other factors

Existing tests (password.test.ts "invalid algorithm throws", pbkdf2.test.ts "invalid inputs > digest") exercise the retained None/'invalid fallthroughs and pass per the PR description. The PR correctly notes a fail-before/pass-after test is structurally impossible for dead-branch removal, which REVIEW.md accepts. The slice lifetime hoist is sound — it lives to end of the 'brk block on both the success (break 'brk) and error (return Err) paths, and Rust's borrow checker enforces this. This aligns with REVIEW.md's "delete dead code in the same PR that makes it dead" and "simplest honest shape" guidance.

@robobun

robobun commented Jul 20, 2026

Copy link
Copy Markdown
Collaborator Author

Closing. This is dead-branch removal with no observable behavior change: the ? on the preceding get_zig_string/to_slice already propagates any exception, and the lookup helpers in between (algorithm_from_zig_string, evp::lookup_ignore_case, Algorithm::md) never enter the VM, so has_exception() is always false at the guard and the alternate arms cannot execute. No input distinguishes before from after, which means a regression test that fails on main and passes here cannot be written.

The two related items from the same audit (the bcrypt & 0x3F mask and the explicit drop() calls in async verify) are already being rewritten by #33865 and #33702 as part of behavioral fixes; those PRs are the better vehicle for any adjacent cleanup.

@robobun robobun closed this Jul 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant